///////////////////////////////////////////////////////////////////////////////
//
//  The contents of this file are subject to the Mozilla Public License
//  Version 1.1 (the "License"); you may not use this file except in
//  compliance with the License. You may obtain a copy of the License at
//  http://www.mozilla.org/MPL/
//
//  Software distributed under the License is distributed on an "AS IS"
//  basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
//  License for the specific language governing rights and limitations
//  under the License.
//
//  The Original Code is MP4v2.
//
//  The Initial Developer of the Original Code is Kona Blend.
//  Portions created by Kona Blend are Copyright (C) 2008.
//  All Rights Reserved.
//
//  Contributors:
//      Kona Blend, kona8lend@@gmail.com
//
///////////////////////////////////////////////////////////////////////////////

#include "libutil/impl.h"

namespace mp4v2
{
    namespace util
    {

        ///////////////////////////////////////////////////////////////////////////////

        Utility::Utility( string name_, int argc_, char **argv_ )
            : _longOptions      ( NULL )
            , _name             ( name_ )
            , _argc             ( argc_ )
            , _argv             ( argv_ )
            , _optimize         ( false )
            , _dryrun           ( false )
            , _keepgoing        ( false )
            , _overwrite        ( false )
            , _force            ( false )
            , _debug            ( 0 )
            , _verbosity        ( 1 )
            , _jobCount         ( 0 )
            , _debugImplicits   ( false )
            , _group            ( "OPTIONS" )

            , STD_OPTIMIZE( 'z', false, "optimize", false, LC_NONE, "optimize mp4 file after modification" )
            , STD_DRYRUN( 'y', false, "dryrun", false, LC_NONE, "do not actually create or modify any files" )
            , STD_KEEPGOING( 'k', false, "keepgoing", false, LC_NONE, "continue batch processing even after errors" )
            , STD_OVERWRITE( 'o', false, "overwrite", false, LC_NONE, "overwrite existing files when creating" )
            , STD_FORCE( 'f', false, "force", false, LC_NONE, "force overwrite even if file is read-only" )
            , STD_QUIET( 'q', false, "quiet", false, LC_NONE, "equivalent to --verbose 0" )
            , STD_DEBUG( 'd', false, "debug", true, LC_DEBUG, "increase debug or long-option to set NUM", "NUM",
                         // 79-cols, inclusive, max desired width
                         // |----------------------------------------------------------------------------|
                         "\nDEBUG LEVELS (for raw mp4 file I/O)"
                         "\n  0  supressed"
                         "\n  1  add warnings and errors (default)"
                         "\n  2  add table details"
                         "\n  3  add implicits"
                         "\n  4  everything" )
            , STD_VERBOSE( 'v', false, "verbose", true, LC_VERBOSE, "increase verbosity or long-option to set NUM", "NUM",
                           // 79-cols, inclusive, max desired width
                           // |----------------------------------------------------------------------------|
                           "\nVERBOSE LEVELS"
                           "\n  0  warnings and errors"
                           "\n  1  normal informative messages (default)"
                           "\n  2  more informative messages"
                           "\n  3  everything" )
            , STD_HELP( 'h', false, "help", false, LC_HELP, "print brief help or long-option for extended help" )
            , STD_VERSION( 0, false, "version", false, LC_VERSION, "print version information and exit" )
            , STD_VERSIONX( 0, false, "versionx", false, LC_VERSIONX, "print extended version information", "ARG", "", true )

        {
            debugUpdate( 1 );

            _usage = "<UNDEFINED>";
            _description = "<UNDEFINED>";
            _groups.push_back( &_group );
        }

        ///////////////////////////////////////////////////////////////////////////////

        Utility::~Utility()
        {
            delete[] _longOptions;
        }

        ///////////////////////////////////////////////////////////////////////////////

        bool
        Utility::batch( int argi )
        {
            _jobCount = 0;
            _jobTotal = _argc - argi;

            // nothing to be done
            if( !_jobTotal )
                return SUCCESS;

            bool batchResult = FAILURE;
            for( int i = argi; i < _argc; i++ )
            {
                bool subResult = FAILURE;
                try
                {
                    if( !job( _argv[i] ))
                    {
                        batchResult = SUCCESS;
                        subResult = SUCCESS;
                    }
                }
                catch( Exception *x )
                {
                    mp4v2::impl::log.errorf(*x);
                    delete x;
                }

                if( !_keepgoing && subResult == FAILURE )
                    return FAILURE;
            }

            return batchResult;
        }

        ///////////////////////////////////////////////////////////////////////////////

        void
        Utility::debugUpdate( uint32_t debug )
        {
            MP4LogLevel level;

            _debug = debug;
            verbose2f( "debug level: %u\n", _debug );

            switch( _debug )
            {
            case 0:
                level = MP4_LOG_NONE;
                _debugImplicits = false;
                break;

            case 1:
                level = MP4_LOG_ERROR;
                _debugImplicits = false;
                break;

            case 2:
                level = MP4_LOG_VERBOSE2;
                _debugImplicits = false;
                break;

            case 3:
                level = MP4_LOG_VERBOSE2;
                _debugImplicits = true;
                break;

            case 4:
            default:
                level = MP4_LOG_VERBOSE4;
                _debugImplicits = true;
                break;
            }

            MP4LogSetLevel(level);
        }

        ///////////////////////////////////////////////////////////////////////////////

        bool
        Utility::dryrunAbort()
        {
            if( !_dryrun )
                return false;

            verbose2f( "skipping: dry-run mode enabled\n" );
            return true;
        }

        ///////////////////////////////////////////////////////////////////////////////

        void
        Utility::errf( const char *format, ... )
        {
            va_list ap;
            va_start( ap, format );
            vfprintf( stderr, format, ap );
            va_end( ap );
        }

        ///////////////////////////////////////////////////////////////////////////////

        void
        Utility::formatGroups()
        {
            // determine longest long-option [+space +argname]
            int longMax = 0;
            list<Group *>::reverse_iterator ie = _groups.rend();
            for( list<Group *>::reverse_iterator it = _groups.rbegin(); it != ie; it++ )
            {
                Group &group = **it;
                const Group::List::const_iterator ieo = group.options.end();
                for( Group::List::const_iterator ito = group.options.begin(); ito != ieo; ito++ )
                {
                    const Option &option = **ito;
                    if( option.hidden )
                        continue;

                    int len = (int)option.lname.length();
                    if( option.lhasarg )
                        len += 1 + (int)option.argname.length();
                    if( len > longMax )
                        longMax = len;
                }
            }

            // format help output (no line-wrapping yet)
            ostringstream oss;

            int groupCount = 0;
            int optionCount = 0;
            ie = _groups.rend();
            for( list<Group *>::reverse_iterator it = _groups.rbegin(); it != ie; it++, groupCount++ )
            {
                if( groupCount )
                    oss << '\n';
                Group &group = **it;
                oss << '\n' << group.name;
                const Group::List::const_iterator ieo = group.options.end();
                for( Group::List::const_iterator ito = group.options.begin(); ito != ieo; ito++, optionCount++ )
                {
                    const Option &option = **ito;
                    if( option.hidden )
                        continue;

                    oss << "\n ";

                    if( option.scode == 0 )
                        oss << "    --";
                    else
                        oss << '-' << option.scode << ", --";

                    if( option.lhasarg )
                    {
                        oss << option.lname << ' ' << option.argname;
                        oss << setw( longMax - option.lname.length() - 1 - option.argname.length() ) << "";
                    }
                    else
                    {
                        oss << setw( longMax ) << left << option.lname;
                    }

                    oss << "  ";

                    const string::size_type imax = option.descr.length();
                    for( string::size_type i = 0; i < imax; i++ )
                        oss << option.descr[i];
                }
            }

            _help = oss.str();

            // allocate and populate C-style options
            delete[] _longOptions;
            _longOptions = new prog::Option[optionCount + 1];

            // fill EOL marker
            _longOptions[optionCount].name = NULL;
            _longOptions[optionCount].type = prog::Option::NO_ARG;
            _longOptions[optionCount].flag = 0;
            _longOptions[optionCount].val  = 0;

            _shortOptions.clear();

            int optionIndex = 0;
            ie = _groups.rend();
            for( list<Group *>::reverse_iterator it = _groups.rbegin(); it != ie; it++ )
            {
                Group &group = **it;
                const Group::List::const_iterator ieo = group.options.end();
                for( Group::List::const_iterator ito = group.options.begin(); ito != ieo; ito++, optionIndex++ )
                {
                    const Option &a = **ito;
                    prog::Option &b = _longOptions[optionIndex];

                    b.name = const_cast<char *>(a.lname.c_str());
                    b.type = a.lhasarg ? prog::Option::REQUIRED_ARG : prog::Option::NO_ARG;
                    b.flag = 0;
                    b.val  = (a.lcode == LC_NONE) ? a.scode : a.lcode;

                    if( a.scode != 0 )
                    {
                        _shortOptions += a.scode;
                        if( a.shasarg )
                            _shortOptions += ':';
                    }
                }
            }
        }

        ///////////////////////////////////////////////////////////////////////////////

        bool
        Utility::job( string arg )
        {
            verbose2f( "job begin: %s\n", arg.c_str() );

            // perform job
            JobContext job( arg );
            bool result = FAILURE;
            try
            {
                result = utility_job( job );
            }
            catch( Exception *x )
            {
                mp4v2::impl::log.errorf(*x);
                delete x;
            }

            // close file handle flagged with job
            if( job.fileHandle != MP4_INVALID_FILE_HANDLE )
            {
                verbose2f( "closing %s\n", job.file.c_str() );
                MP4Close( job.fileHandle );

                // invoke optimize if flagged
                if( _optimize && job.optimizeApplicable )
                {
                    verbose1f( "optimizing %s\n", job.file.c_str() );
                    if( !MP4Optimize( job.file.c_str(), NULL ))
                        hwarnf( "optimize failed: %s\n", job.file.c_str() );
                }
            }

            // free data flagged with job
            list<void *>::iterator ie = job.tofree.end();
            for( list<void *>::iterator it = job.tofree.begin(); it != ie; it++ )
                free( *it );


            verbose2f( "job end\n" );
            _jobCount++;
            return result;
        }

        ///////////////////////////////////////////////////////////////////////////////

        bool
        Utility::herrf( const char *format, ... )
        {
            va_list ap;
            va_start( ap, format );

            if( _keepgoing )
            {
                fprintf( stdout, "WARNING: " );
                vfprintf( stdout, format, ap );
            }
            else
            {
                fprintf( stderr, "ERROR: " );
                vfprintf( stderr, format, ap );
            }

            va_end( ap );
            return FAILURE;
        }

        ///////////////////////////////////////////////////////////////////////////////

        bool
        Utility::hwarnf( const char *format, ... )
        {
            fprintf( stdout, "WARNING: " );
            va_list ap;
            va_start( ap, format );
            vfprintf( stdout, format, ap );
            va_end( ap );
            return FAILURE;
        }

        ///////////////////////////////////////////////////////////////////////////////

        void
        Utility::outf( const char *format, ... )
        {
            va_list ap;
            va_start( ap, format );
            vfprintf( stdout, format, ap );
            va_end( ap );
        }

        ///////////////////////////////////////////////////////////////////////////////

        void
        Utility::printHelp( bool extended, bool toerr )
        {
            ostringstream oss;
            oss << "Usage: " << _name << " " << _usage << '\n' << _description << '\n' << _help;

            if( extended )
            {
                const list<Group *>::reverse_iterator ie = _groups.rend();
                for( list<Group *>::reverse_iterator it = _groups.rbegin(); it != ie; it++ )
                {
                    Group &group = **it;
                    const Group::List::const_iterator ieo = group.options.end();
                    for( Group::List::const_iterator ito = group.options.begin(); ito != ieo; ito++ )
                    {
                        const Option &option = **ito;
                        if( option.help.empty() )
                            continue;

                        oss << '\n' << option.help;
                    }
                }
            }

            if( toerr )
                errf( "%s\n\n", oss.str().c_str() );
            else
                outf( "%s\n\n", oss.str().c_str() );
        }

        ///////////////////////////////////////////////////////////////////////////////

        void
        Utility::printUsage( bool toerr )
        {
            ostringstream oss;
            oss <<   "Usage: " << _name << " " << _usage
                << "\nTry -h for brief help or --help for extended help";

            if( toerr )
                errf( "%s\n", oss.str().c_str() );
            else
                outf( "%s\n", oss.str().c_str() );
        }

        ///////////////////////////////////////////////////////////////////////////////

        void
        Utility::printVersion( bool extended )
        {
            ostringstream oss;
            oss << left;

            if( extended )
            {
                oss <<         setw(13) << "utility:" << _name
                    << '\n' << setw(13) << "product:" << MP4V2_PROJECT_name
                    << '\n' << setw(13) << "version:" << MP4V2_PROJECT_version
                    << '\n' << setw(13) << "build date:" << MP4V2_PROJECT_build
                    << '\n'
                    << '\n' << setw(18) << "repository URL:" << MP4V2_PROJECT_repo_url
                    << '\n' << setw(18) << "repository root:" << MP4V2_PROJECT_repo_root
                    << '\n' << setw(18) << "repository UUID:" << MP4V2_PROJECT_repo_uuid
                    << '\n' << setw(18) << "repository rev:" << MP4V2_PROJECT_repo_rev
                    << '\n' << setw(18) << "repository date:" << MP4V2_PROJECT_repo_date
                    << '\n' << setw(18) << "repository type:" << MP4V2_PROJECT_repo_type;
            }
            else
            {
                oss << _name << " - " << MP4V2_PROJECT_name_formal;
            }

            outf( "%s\n", oss.str().c_str() );
        }

        ///////////////////////////////////////////////////////////////////////////////

        bool
        Utility::process()
        {
            bool rv = true;

            try
            {
                rv = process_impl();
            }
            catch( Exception *x )
            {
                _keepgoing = false;
                mp4v2::impl::log.errorf(*x);
                delete x;
            }

            return rv;
        }

        ///////////////////////////////////////////////////////////////////////////////

        bool
        Utility::process_impl()
        {
            formatGroups();

            // populate code lookup set
            set<int> codes;
            const Group::List::const_iterator ie = _group.options.end();
            for( Group::List::const_iterator it = _group.options.begin(); it != ie; it++ )
            {
                const Option &option = **it;
                if( option.scode != 0 )
                    codes.insert( option.scode );
                if( option.lcode != LC_NONE )
                    codes.insert( option.lcode );
            }

            for( ;; )
            {
                const int code = prog::getOption( _argc, _argv, _shortOptions.c_str(), _longOptions, NULL );
                if( code == -1 )
                    break;

                bool handled = false;
                if( utility_option( code, handled ))
                    return FAILURE;
                if( handled )
                    continue;

                if( codes.find( code ) == codes.end() )
                    continue;

                switch( code )
                {
                case 'z':
                    _optimize = true;
                    break;

                case 'y':
                    _dryrun = true;
                    break;

                case 'k':
                    _keepgoing = true;
                    break;

                case 'o':
                    _overwrite = true;
                    break;

                case 'f':
                    _force = true;
                    break;

                case 'q':
                    _verbosity = 0;
                    debugUpdate( 0 );
                    break;

                case 'v':
                    _verbosity++;
                    break;

                case 'd':
                    debugUpdate( _debug + 1 );
                    break;

                case 'h':
                    printHelp( false, false );
                    return SUCCESS;

                case LC_DEBUG:
                    debugUpdate( std::strtoul( prog::optarg, NULL, 0 ) );
                    break;

                case LC_VERBOSE:
                {
                    const uint32_t level = std::strtoul( prog::optarg, NULL, 0 );
                    _verbosity = ( level < 4 ) ? level : 3;
                    break;
                }

                case LC_HELP:
                    printHelp( true, false );
                    return SUCCESS;

                case LC_VERSION:
                    printVersion( false );
                    return SUCCESS;

                case LC_VERSIONX:
                    printVersion( true );
                    return SUCCESS;

                default:
                    printUsage( true );
                    return FAILURE;
                }
            }

            if( !(prog::optind < _argc) )
            {
                printUsage( true );
                return FAILURE;
            }

            const bool result = batch( prog::optind );
            verbose2f( "exit code %d\n", result );
            return result;
        }

        ///////////////////////////////////////////////////////////////////////////////

        bool
        Utility::openFileForWriting( io::File &file )
        {
            // simple case is file does not exist
            if( !io::FileSystem::exists( file.name ))
            {
                if( file.open() )
                    return herrf( "unable to open %s for write: %s\n", file.name.c_str(), sys::getLastErrorStr() );
                return SUCCESS;
            }

            // fail if overwrite is not enabled
            if( !_overwrite )
                return herrf( "file already exists: %s\n", file.name.c_str() );

            // only overwrite if it is a file
            if( !io::FileSystem::isFile( file.name ))
                return herrf( "cannot overwrite non-file: %s\n", file.name.c_str() );

            // first attemp to re-open/truncate so as to keep any file perms
            if( !file.open() )
                return SUCCESS;

            // fail if force is not enabled
            if( !_force )
                return herrf( "unable to overwrite file: %s\n", file.name.c_str() );

            // first attempt to open, truncating file
            if( !file.open() )
                return SUCCESS;

            // nuke file
            if( ::remove( file.name.c_str() ))
                return herrf( "unable to remove %s: %s\n", file.name.c_str(), sys::getLastErrorStr() );

            // final effort
            if( !file.open() )
                return SUCCESS;

            return herrf( "unable to open %s for write: %s\n", file.name.c_str(), sys::getLastErrorStr() );
        }

        ///////////////////////////////////////////////////////////////////////////////

        void
        Utility::verbose( uint32_t level, const char *format, va_list ap )
        {
            if( level > _verbosity )
                return;
            vfprintf( stdout, format, ap );
        }

        ///////////////////////////////////////////////////////////////////////////////

        void
        Utility::verbose1f( const char *format, ... )
        {
            va_list ap;
            va_start( ap, format );
            verbose( 1, format, ap );
            va_end( ap );
        }

        ///////////////////////////////////////////////////////////////////////////////

        void
        Utility::verbose2f( const char *format, ... )
        {
            va_list ap;
            va_start( ap, format );
            verbose( 2, format, ap );
            va_end( ap );
        }

        ///////////////////////////////////////////////////////////////////////////////

        void
        Utility::verbose3f( const char *format, ... )
        {
            va_list ap;
            va_start( ap, format );
            verbose( 3, format, ap );
            va_end( ap );
        }

        ///////////////////////////////////////////////////////////////////////////////

        const bool Utility::SUCCESS = false;
        const bool Utility::FAILURE = true;

        ///////////////////////////////////////////////////////////////////////////////

        Utility::Group::Group( string name_ )
            : name    ( name_ )
            , options ( _options )
        {
        }

        ///////////////////////////////////////////////////////////////////////////////

        Utility::Group::~Group()
        {
            const List::iterator ie = _optionsDelete.end();
            for( List::iterator it = _optionsDelete.begin(); it != ie; it++ )
                delete *it;
        }

        ///////////////////////////////////////////////////////////////////////////////

        void
        Utility::Group::add( const Option &option )
        {
            _options.push_back( &option );
        }

        ///////////////////////////////////////////////////////////////////////////////

        void
        Utility::Group::add(
            char     scode,
            bool     shasarg,
            string   lname,
            bool     lhasarg,
            uint32_t lcode,
            string   descr,
            string   argname,
            string   help,
            bool     hidden )
        {
            Option *o = new Option( scode, shasarg, lname, lhasarg, lcode, descr, argname, help, hidden );
            _options.push_back( o );
            _optionsDelete.push_back( o );
        }

        ///////////////////////////////////////////////////////////////////////////////

        void
        Utility::Group::add(
            string   lname,
            bool     lhasarg,
            uint32_t lcode,
            string   descr,
            string   argname,
            string   help,
            bool     hidden )
        {
            add( 0, false, lname, lhasarg, lcode, descr, argname, help, hidden );
        }

        ///////////////////////////////////////////////////////////////////////////////

        Utility::Option::Option(
            char     scode_,
            bool     shasarg_,
            string   lname_,
            bool     lhasarg_,
            uint32_t lcode_,
            string   descr_,
            string   argname_,
            string   help_,
            bool     hidden_ )
            : scode   ( scode_ )
            , shasarg ( shasarg_ )
            , lname   ( lname_ )
            , lhasarg ( lhasarg_ )
            , lcode   ( lcode_ )
            , descr   ( descr_ )
            , argname ( argname_ )
            , help    ( help_ )
            , hidden  ( hidden_ )
        {
        }

        ///////////////////////////////////////////////////////////////////////////////

        Utility::JobContext::JobContext( string file_ )
            : file               ( file_ )
            , fileHandle         ( MP4_INVALID_FILE_HANDLE )
            , optimizeApplicable ( false )
        {
        }

        ///////////////////////////////////////////////////////////////////////////////

    }
} // namespace mp4v2::util
